Skip to content

feat(fuzz): enhance corpus mutation with all-call strategy and msg.value support#13177

Draft
grandizzy wants to merge 8 commits intofoundry-rs:masterfrom
grandizzy:feat/constraint-guided-abi-mutation
Draft

feat(fuzz): enhance corpus mutation with all-call strategy and msg.value support#13177
grandizzy wants to merge 8 commits intofoundry-rs:masterfrom
grandizzy:feat/constraint-guided-abi-mutation

Conversation

@grandizzy
Copy link
Copy Markdown
Collaborator

@grandizzy grandizzy commented Jan 22, 2026

Motivation

This PR adds enhancements to the corpus-based invariant fuzzer:

  1. Unified mutation strategies: Renamed Prefix/Suffix/Abi to GenPrefix/GenSuffix/GenMutate for clarity. GenMutate now mutates a random number of calls (1 to all) using shuffled indices, similar to how GenPrefix generates a random prefix of new calls.

  2. Random msg.value for payable functions: Currently, Foundry invariant fuzz testing does not support transactions with value > 0, which makes it unable to find certain bug sequences. This adds automatic msg.value generation for payable functions.

  3. max_deal config option: New invariant config to fund senders before payable function calls, enabling msg.value > 0 transactions without requiring manual vm.deal() in setUp.

Solution

Mutation Strategy Unification

  • Renamed mutation types: Prefix -> GenPrefix, Suffix -> GenSuffix, Abi -> GenMutate
  • GenMutate now picks a random count (1 to all) of calls to mutate, using Fisher-Yates shuffle to select which calls
  • More gradual mutation than the previous 30% all / 70% single approach

msg.value Support (based on PR #8644)

  • Initial call generation (fuzz_contract_with_calldata): For payable functions, generates random value ~15% of the time
  • Corpus mutation (abi_mutate): During ABI mutation, mutates sender (15%) and msg.value (15% for payable functions)
  • Display support: Shows value in both regular output (value=X) and Solidity output ({value: X}) formats

max_deal Config Option

New [invariant] config option to automatically fund senders:

[invariant]
max_deal = 1000000000000000000  # 1 ETH in wei

Behavior:

  • If max_deal is NOT configured: Value is generated for payable functions but falls back to 0 if sender has insufficient balance
  • If max_deal IS configured: Random deal amount (0 to max_deal) is generated and applied before each payable call, enabling msg.value > 0 transactions to succeed

Implementation details:

  • Deal is only applied when the function is payable (has value > 0)
  • If balance is still insufficient after deal, value falls back to 0
  • Counterexamples display deal as:
    • Regular: deal=X
    • Solidity: vm.deal(sender, sender.balance + X);

Value Generation Strategy

Biased towards smaller values to avoid sender balance issues:

  • 85% chance: no value (None)
  • 10% chance: small values (0-1000 wei)
  • 4% chance: medium values (up to 0.001 ETH)
  • 1% chance: larger values (up to 1 ETH)

Changes

  • Rename mutation types to GenPrefix/GenSuffix/GenMutate
  • Add deal: Option<U256> field to BasicTxDetails and BaseCounterExample
  • Add max_deal: Option<u64> to InvariantConfig
  • Add value: Option<U256> field to CallDetails and BaseCounterExample
  • Add shared value generation helpers: fuzz_msg_value() and generate_msg_value()
  • Add sender mutation (15% probability) using addresses from dictionary
  • Update execute_tx to apply deal before call and pass value to call_raw
  • Update display formatters to show deal and value in sequence output
  • Add backwards-compatible serde support for corpus files
  • Add CLI tests for max_deal functionality

Testing

Tested against the Daedaluzz maze invariant test suite where this change helped break additional invariants by ensuring consistent parameter patterns across the entire call sequence.

Credits

Based on #8644 by @QiuhaoLi

Co-authored-by: QiuhaoLi qiuhaoli@outlook.com
Co-authored-by: Amp amp@ampcode.com

@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from f9e780c to ea0af53 Compare January 22, 2026 07:52
@grandizzy grandizzy changed the title feat(fuzz): add all-call ABI mutation strategy for invariant testing feat(fuzz): enhance corpus mutation with all-call strategy and msg.value support Jan 22, 2026
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from ea0af53 to 622c18a Compare January 22, 2026 08:04
…lue support

When using the ABI mutation type in coverage-guided fuzzing:
- 30% chance to mutate ALL calls in the sequence rather than just one
- Mutate sender (15%) using addresses from dictionary
- Mutate msg.value (15%) for payable functions

Also adds automatic msg.value generation for payable functions during
initial call generation, with value shown in sequence output.

Value generation is biased towards smaller values to avoid balance issues:
- 85% no value, 10% small (0-1000 wei), 4% medium (0.001 ETH), 1% large (1 ETH)

Based on foundry-rs#8644

Co-authored-by: QiuhaoLi <qiuhaoli@outlook.com>
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from 622c18a to 1465ae0 Compare January 22, 2026 08:16
@grandizzy
Copy link
Copy Markdown
Collaborator Author

@0xalpharush mind to quickly check this? thank you!

) -> Result<()> {
// let rng = test_runner.rng();
// Mutate sender with 15% probability using addresses from dictionary.
if test_runner.rng().random_ratio(15, 100) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also check targeted/excluded senders as was done here #13090?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in d452197

Comment thread crates/evm/evm/src/executors/corpus.rs Outdated
// 30% chance to mutate ALL calls in the sequence.
// This helps break multi-constraint bugs where any call could hit the target.
if rng.random_range(0..10) < 3 {
for tx in &mut new_seq {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Prefix mutation is essentially this but a subset and generation instead of mutation. Maybe we should have GenPrefix and GenMutate and mutate up to every element, but not always every element

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, will merge with prefix mutations

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed 40fd045

Copy link
Copy Markdown
Contributor

@0xalpharush 0xalpharush Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name GenMutate was a mistake on my part and the old name Abi was better. The implementation is fine though.

What I was suggesting was to add two new mutators, MutatePrefix and MutateSuffix, where gen means call new_tx and mutate means use a tx in the seq and mutate it with abi_mutate (optionally you can have even an identity one which clones existing txs) . Here is a patch to reduce confusion. It also fixes the repeat mutation to insert instead of splice (which overwrites) as well as adds a swap and delete mutation.

@0xalpharush
Copy link
Copy Markdown
Contributor

For the call value, is there any reason it can't be much larger occasionally and we set the balance in the DB, minting the ETH effecitvely?

@grandizzy
Copy link
Copy Markdown
Collaborator Author

grandizzy commented Jan 22, 2026

For the call value, is there any reason it can't be much larger occasionally and we set the balance in the DB, minting the ETH effecitvely?

I wanted to avoid minting / setting balance directly in db and let tester control that by using the deal cheatcode as could be confusing in a scenario with sender that starts with a certain balance and expected to go down during tests but ends up with a higher one . Lmk if this makes sense

@0xalpharush
Copy link
Copy Markdown
Contributor

0xalpharush commented Jan 22, 2026

I wanted to avoid minting / setting balance directly in db and let tester control that by using the deal cheatcode as could be confusing in a scenario with sender that starts with a certain balance and expected to go down during tests but ends up with a higher one . Lmk if this makes sense

That makes sense. Maybe like how you added warp and roll (#12616), we can also add a deal before txs to increase the balance. And the mutator should be able to mutate these calls as if it were an ABI encoded to the respective signature

@grandizzy grandizzy self-assigned this Jan 23, 2026
grandizzy added a commit to grandizzy/foundry that referenced this pull request Jan 23, 2026
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation
in abi_mutate now uses select_random_sender_for_mutation() which respects
targetSender()/excludeSender() invariant test configurations, similar to
how PR foundry-rs#13090 implemented it for address mutations.

Changes:
- Added select_random_sender_for_mutation() in strategies/param.rs
- Added Clone derive to SenderFilters
- Updated abi_mutate() to accept optional SenderFilters parameter
- Store sender_filters in InvariantTest and pass to new_inputs()
- Redacted test values in invariant_warp_and_roll and
  invariant_optimization_with_warp for CI stability

Amp-Thread-ID: https://ampcode.com/threads/T-019beba0-989c-764a-b912-79aef0b14951
Co-authored-by: Amp <amp@ampcode.com>
grandizzy added a commit to grandizzy/foundry that referenced this pull request Jan 26, 2026
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation
in abi_mutate now uses select_random_sender_for_mutation() which respects
targetSender()/excludeSender() invariant test configurations, similar to
how PR foundry-rs#13090 implemented it for address mutations.

Changes:
- Added select_random_sender_for_mutation() in strategies/param.rs
- Added Clone derive to SenderFilters
- Updated abi_mutate() to accept optional SenderFilters parameter
- Store sender_filters in InvariantTest and pass to new_inputs()
- Redacted test values in invariant_warp_and_roll and
  invariant_optimization_with_warp for CI stability

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf93d-da9d-7669-a396-76401d31ec5e
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from 9e1e715 to a9cb950 Compare January 26, 2026 08:55
grandizzy added a commit to grandizzy/foundry that referenced this pull request Jan 26, 2026
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation
in abi_mutate now uses select_random_sender_for_mutation() which respects
targetSender()/excludeSender() invariant test configurations, similar to
how PR foundry-rs#13090 implemented it for address mutations.

Changes:
- Added select_random_sender_for_mutation() in strategies/param.rs
- Added Clone derive to SenderFilters
- Updated abi_mutate() to accept optional SenderFilters parameter
- Store sender_filters in InvariantTest and pass to new_inputs()
- Redacted test values in invariant_warp_and_roll and
  invariant_optimization_with_warp for CI stability

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf93d-da9d-7669-a396-76401d31ec5e
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from a9cb950 to 0a2d01a Compare January 26, 2026 08:59
grandizzy added a commit to grandizzy/foundry that referenced this pull request Jan 26, 2026
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation
in abi_mutate now uses select_random_sender_for_mutation() which respects
targetSender()/excludeSender() invariant test configurations, similar to
how PR foundry-rs#13090 implemented it for address mutations.

Changes:
- Added select_random_sender_for_mutation() in strategies/param.rs
- Added Clone derive to SenderFilters
- Updated abi_mutate() to accept optional SenderFilters parameter
- Store sender_filters in InvariantTest and pass to new_inputs()
- Redacted test values in invariant_warp_and_roll and
  invariant_optimization_with_warp for CI stability

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf93d-da9d-7669-a396-76401d31ec5e
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from 0a2d01a to d1b422a Compare January 26, 2026 09:08
Addresses @0xalpharush's review comment on PR foundry-rs#13177: the sender mutation
in abi_mutate now uses select_random_sender_for_mutation() which respects
targetSender()/excludeSender() invariant test configurations, similar to
how PR foundry-rs#13090 implemented it for address mutations.

Changes:
- Added select_random_sender_for_mutation() in strategies/param.rs
- Added Clone derive to SenderFilters
- Updated abi_mutate() to accept optional SenderFilters parameter
- Store sender_filters in InvariantTest and pass to new_inputs()
- Redacted test values in invariant_warp_and_roll and
  invariant_optimization_with_warp for CI stability

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf93d-da9d-7669-a396-76401d31ec5e
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from d1b422a to d452197 Compare January 26, 2026 09:10
grandizzy and others added 2 commits January 26, 2026 11:59
Addresses @0xalpharush's review comment on PR foundry-rs#13177: unify mutation
strategies by renaming Prefix/Suffix/Abi to GenPrefix/GenSuffix/GenMutate.

GenMutate now mutates a random number of calls (1 to all) using shuffled
indices, similar to how GenPrefix generates a random prefix of new calls.
This is more gradual than the previous 30% all / 70% single approach.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf992-367c-721c-a497-58a1a11ee2be
Adds max_deal invariant config option that generates random deal amounts
(0 to max_deal) to increase sender balance before each tx for payable functions.

Behavior:
- If max_deal is NOT configured: value is generated for payable functions but
  falls back to 0 if sender has insufficient balance
- If max_deal IS configured: random deal amount is applied before the call,
  enabling payable calls with msg.value > 0 to succeed

The deal is only applied when the function is payable (has value > 0).
If balance is still insufficient after deal, value falls back to 0.

Counterexamples display deal as:
- Regular: deal=X
- Solidity: vm.deal(sender, sender.balance + X);

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019bf992-367c-721c-a497-58a1a11ee2be
@grandizzy grandizzy force-pushed the feat/constraint-guided-abi-mutation branch from 6ad1403 to 6ce3a1e Compare January 26, 2026 12:16
@grandizzy
Copy link
Copy Markdown
Collaborator Author

I wanted to avoid minting / setting balance directly in db and let tester control that by using the deal cheatcode as could be confusing in a scenario with sender that starts with a certain balance and expected to go down during tests but ends up with a higher one . Lmk if this makes sense

That makes sense. Maybe like how you added warp and roll (#12616), we can also add a deal before txs to increase the balance. And the mutator should be able to mutate these calls as if it were an ABI encoded to the respective signature

added max_deal config and vm.deal in counterexample with 6ce3a1e

@grandizzy grandizzy requested a review from 0xalpharush January 26, 2026 12:24
@grandizzy
Copy link
Copy Markdown
Collaborator Author

@0xalpharush this is rfr, please recheck. thank you!

@grandizzy grandizzy marked this pull request as ready for review January 26, 2026 12:24
show_solidity: false,
max_time_delay: None,
max_block_delay: None,
max_deal: None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily in this PR, but maybe we should provides Some(..) defaults for max_time_delay, max_block_delay, and max_deal.

let len = new_seq.len();
// Mutate a random number of calls (1 to all), similar to how GenPrefix
// generates a random number of new calls.
let n_to_mutate = rng.random_range(1..=len);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if multiple args of the same tx can be mutated. I think right now, only one per tx can mutated even if n_to_mutate > 1

@grandizzy grandizzy marked this pull request as draft February 1, 2026 20:17
@grandizzy
Copy link
Copy Markdown
Collaborator Author

@0xalpharush thank you, going to address your comments asap!

executor.set_balance(tx.sender, current_balance + deal)?;
}

// Only use value if sender has sufficient balance (after deal), otherwise fall back to 0.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just do sender_balance.min(requested_value) instead of sending zero?

@0xalpharush
Copy link
Copy Markdown
Contributor

I would prefer holding off on "Unified mutation strategies", finalizing and merging #12587, and splitting out msg.value support into its own PR. That way, we will be able to more easily gauge changes e.g. 0xalpharush#7 with scfuzzbench.

@grandizzy
Copy link
Copy Markdown
Collaborator Author

I would prefer holding off on "Unified mutation strategies", finalizing and merging #12587, and splitting out msg.value support into its own PR. That way, we will be able to more easily gauge changes e.g. 0xalpharush#7 with scfuzzbench.

I would prefer holding off on "Unified mutation strategies", finalizing and merging #12587, and splitting out msg.value support into its own PR. That way, we will be able to more easily gauge changes e.g. 0xalpharush#7 with scfuzzbench.

#12587 is rfr, ptal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants